Fix that ViFindBrace doesn't search for brace when current char is not a brace#4862
Fix that ViFindBrace doesn't search for brace when current char is not a brace#4862sharpchen wants to merge 2 commits intoPowerShell:masterfrom
Conversation
|
S |
|
You are right that's how it works. I'm hoping we can do two things: verify the order of precedence in vanilla Vi (I was playing around trying to determine it and couldn't make heads or tails of it), and add some unit tests. Thanks! Love the fix though. |
Not sure what it means? If you mean the standard behavior,
And
It indeed requires some tests. I did find at least one inconsistent behavior. |
|
Hi @andyleejordan. I've rewrote the implementation, I think it's now good to go. |
| // find next of any kind of paren | ||
| for (; nextParen < _buffer.Length; nextParen++) | ||
| for (int idx = 0; idx < parenthese.Length; idx++) | ||
| if (parenthese[idx] == _buffer[nextParen]) goto Outer; |
There was a problem hiding this comment.
The rare great use of goto
|
I'm not sure but if this build is like others (looking at PSScriptAnalyzer right now), AppVeyor is at least running tests, looks like |
611243c to
fee3566
Compare
|
Looks like tests are only available on Windows.
I honestly have no idea why test's failing. There's is a input but claims no key. |
I forgot about that but yes this is unfortunately true. |
|
@sharpchen I will get this built on Windows and see if I can't fix the test so we can get this in! |
|
Claude Opus 4.6 was actualy very adapt at fixing this error (it helps that the test was failing for the same reason as other similar tests, which then already had a fix it applied). |
These tests need to be cleared with `ddi` and then asserted to be empty. It's the same fix for the other tests whose input contains unmatched braces. Copilot's plausible explanation is that the unmatched braces cause the input to be flagged by PowerShell's parser as incomplete, so `AcceptLineImpl` instead waits for more input, causing the exception.
48bf1d5 to
85ec2c2
Compare
andyleejordan
left a comment
There was a problem hiding this comment.
LGTM let's get it in @daxian-dbw
There was a problem hiding this comment.
Pull request overview
This PR updates vi-mode brace matching so that when the cursor is not on a brace/paren/bracket, % searches forward for the next one and then jumps to its match (affecting ViGotoBrace, ViDeleteBrace, etc.).
Changes:
- Extend
ViFindBraceto scan forward for the next{ } ( ) [ ]when the current character is not a brace. - Add new
ViGotoBracetest cases covering “cursor not on a brace” scenarios and unmatched-brace behavior. - Minor test-file formatting/comment adjustments.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| PSReadLine/Movement.vi.cs | Implements the new “scan forward for next brace” behavior in ViFindBrace. |
| test/MovementTest.VI.cs | Adds/extends xUnit coverage for % behavior when the cursor is not positioned on a brace. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (int idx = 0; idx < parenthese.Length; idx++) | ||
| if (parenthese[idx] == _buffer[nextParen]) goto Outer; | ||
|
|
||
| Outer: |
There was a problem hiding this comment.
In the default case, if no brace/paren/bracket is found from the current position to the end of the buffer, nextParen will end up equal to _buffer.Length, and _buffer[nextParen] will throw IndexOutOfRangeException. Add an explicit check after the scan (e.g., if nextParen >= _buffer.Length return i) before indexing into _buffer.
| Outer: | |
| Outer: | |
| if (nextParen >= _buffer.Length) | |
| { | |
| return i; | |
| } |
There was a problem hiding this comment.
In the default case, if no brace/paren/bracket is found from the current position to the end of the buffer,
nextParenwill end up equal to_buffer.Length, and_buffer[nextParen]will throwIndexOutOfRangeException
This is a real problem. I prefer to not using the Outer label here. For the goto statement, we should avoid using it unless it's really needed.
| ReadOnlySpan<char> parenthese = stackalloc char[] { '{', '}', '(', ')', '[', ']' }; | ||
| int nextParen = i; | ||
| // find next of any kind of paren | ||
| for (; nextParen < _buffer.Length; nextParen++) | ||
| for (int idx = 0; idx < parenthese.Length; idx++) | ||
| if (parenthese[idx] == _buffer[nextParen]) goto Outer; |
There was a problem hiding this comment.
Variable name parenthese appears to be a typo (should be parentheses or similar). Renaming would improve readability and avoid spreading the misspelling.
| ReadOnlySpan<char> parenthese = stackalloc char[] { '{', '}', '(', ')', '[', ']' }; | |
| int nextParen = i; | |
| // find next of any kind of paren | |
| for (; nextParen < _buffer.Length; nextParen++) | |
| for (int idx = 0; idx < parenthese.Length; idx++) | |
| if (parenthese[idx] == _buffer[nextParen]) goto Outer; | |
| ReadOnlySpan<char> parentheses = stackalloc char[] { '{', '}', '(', ')', '[', ']' }; | |
| int nextParen = i; | |
| // find next of any kind of paren | |
| for (; nextParen < _buffer.Length; nextParen++) | |
| for (int idx = 0; idx < parentheses.Length; idx++) | |
| if (parentheses[idx] == _buffer[nextParen]) goto Outer; |
| using System.Collections.Generic; | ||
| using Xunit; |
There was a problem hiding this comment.
using System.Collections.Generic; appears unused in this file (no List, Dictionary, etc.). Please remove it to avoid compiler warnings and keep usings minimal.
| using System.Collections.Generic; | |
| using Xunit; | |
| using Xunit; |
| // Closing paren without backward match | ||
| string input2 = $"0]2)4foo{closing}"; | ||
| Test(input2, Keys( | ||
| input2, | ||
| CheckThat(() => AssertCursorLeftIs(9)), | ||
| _.Escape, CheckThat(() => AssertCursorLeftIs(8)), | ||
| "0ff", CheckThat(() => AssertCursorLeftIs(5)), | ||
| _.Percent, CheckThat(() => AssertCursorLeftIs(5)), // stay still | ||
| _.Percent, CheckThat(() => AssertCursorLeftIs(5)) | ||
| )); |
There was a problem hiding this comment.
In the "Closing paren without backward match" case, % should still trigger a ding (consistent with other unmatched-brace cases in this test and with ViGotoBrace dinging when ViFindBrace returns the current position). Consider using TestMustDing here so the test actually asserts the expected ding behavior, not just that the cursor stays still.
| for (; nextParen < _buffer.Length; nextParen++) | ||
| for (int idx = 0; idx < parenthese.Length; idx++) |
There was a problem hiding this comment.
We can find the next parenthesis by the code below, instead of using nested loop here:
ReadOnlySpan<char> parens = stackalloc char[] { '{', '}', '(', ')', '[', ']' };
// Find next parenthesis of any type
int idx = _buffer.AsSpan(i).IndexOfAny(parens);
if (idx < 0)
{
// No parenthesis, so nothing to match
return i;
}
int nextParen = i + rel;
int match = _buffer[nextParen] switch
{
...
}
return match == nextParen ? i : match;
PR Summary
Vi searches right pair of first left brace, or first right brace when current char is not a brace. This pr added this functionality.
After the update,

ViGotoBraceshould work like this(of course this will also affect other methods such asViDeleteBrace):PR Checklist
Microsoft Reviewers: Open in CodeFlow